﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Threading;
using System.Net;
using System.IO;
using System.Reflection;
using System.Diagnostics;

namespace WebPALTT.Lib
{
    public class ThreadWorker
    {
        public ManualResetEvent ThreadsFinished;
        static int iThreadCount = 0;
        static int iMaxCount = 0;
        Assembly mCompiledAssembly;
        List<KeyValuePair<String, String>> mLog;
        List<Scenario> mResults;

        public Scenario[] Results { get { return mResults.ToArray(); } }
        public KeyValuePair<String, String>[] Log { get { return mLog.ToArray(); } }

        public ThreadWorker(int maxCount, Assembly compiledAssembly)
        {
            iMaxCount = maxCount;
            iThreadCount = 0;
            mCompiledAssembly = compiledAssembly;
            mLog = new List<KeyValuePair<string, string>> { };
            mResults = new List<Scenario> { };
        }

        /// <summary>
        /// Resets the current thread count
        /// </summary>
        public void Reset()
        {
            iThreadCount = 0;
        }

        public void SceneWorker(object obj)
        {
            Scenario scene = null;
            try
            {
                scene = (Scenario)((object[])obj).GetValue(0);
                String className = (String)((object[])obj).GetValue(1);

                VirtualWorker worker = null;
                ManualResetEvent virtualThreadsFinished = new ManualResetEvent(false);
                switch (scene.RunConditions.RunMode)
                {
                    case RunMode.Concurrent:
                        worker = new VirtualWorker(scene.RunConditions.VirtualUsers, mCompiledAssembly);
                        worker.ThreadsFinished = virtualThreadsFinished;

                        for (int i = 0; i < scene.RunConditions.VirtualUsers; i++)
                        {
                            Scenario clone = ObjectCopier.Clone<Scenario>(scene);
                            clone.Name += " (" + (i + 1) + ")";
                            ThreadPool.QueueUserWorkItem(new WaitCallback(worker.Worker), new object[] { clone, className });
                        }

                        virtualThreadsFinished.WaitOne(Timeout.Infinite, true);
                        break;
                    case RunMode.Sequential:
                        worker = new VirtualWorker(1, mCompiledAssembly);
                        worker.ThreadsFinished = virtualThreadsFinished;

                        for (int i = 0; i < scene.RunConditions.VirtualUsers; i++)
                        {
                            worker.Reset();
                            Scenario clone = ObjectCopier.Clone<Scenario>(scene);
                            clone.Name += " (" + (i + 1) + ")";
                            ThreadPool.QueueUserWorkItem(new WaitCallback(worker.Worker), new object[] { clone, className });
                            virtualThreadsFinished.WaitOne(Timeout.Infinite, true);
                        }
                        break;
                    case RunMode.RampUp:
                        worker = new VirtualWorker(scene.RunConditions.StartingUsers, mCompiledAssembly);
                        worker.ThreadsFinished = virtualThreadsFinished;

                        DateTime testEnd = DateTime.Now.AddMinutes(scene.RunConditions.MaxTime);
                        int usersRun = scene.RunConditions.StartingUsers;
                        for (int i = 0; i < scene.RunConditions.StartingUsers; i++)
                        {
                            Scenario clone = ObjectCopier.Clone<Scenario>(scene);
                            clone.Name += " (" + (i + 1) + ")";
                            ThreadPool.QueueUserWorkItem(new WaitCallback(worker.Worker), new object[] { clone, className });
                        }

                        while (true)
                        {
                            Thread.Sleep(scene.RunConditions.RampUpTimeSpan);

                            if (scene.RunConditions.StopOnUserCount && usersRun >= scene.RunConditions.VirtualUsers)
                                break;
                            else if (scene.RunConditions.StopOnMaxTime && testEnd.CompareTo(DateTime.Now) >= 0)
                                break;
                            else if (!scene.RunConditions.StopOnUserCount && !scene.RunConditions.StopOnMaxTime)
                                break; // Error in Definition

                            usersRun += scene.RunConditions.RampUpUsers;
                            worker.IncreaseMaxCount(scene.RunConditions.RampUpUsers);
                            for (int i = 0; i < scene.RunConditions.RampUpUsers; i++)
                            {
                                Scenario clone = ObjectCopier.Clone<Scenario>(scene);
                                clone.Name += " (" + (i + 1) + ")";
                                ThreadPool.QueueUserWorkItem(new WaitCallback(worker.Worker), new object[] { clone, className });
                            }
                        }
                        virtualThreadsFinished.WaitOne(Timeout.Infinite, true);
                        break;
                }
                mLog.AddRange(worker.Log);
                mResults.AddRange(worker.Results);
            }
            catch (Exception e)
            {
                mLog.Add(new KeyValuePair<string, string>("ERROR Creating Virtual Users: <" + (scene == null ? "null" : scene.Name) + ">",
                    e.Message + "\r\n\r\n" + e.StackTrace));
            }
            finally
            {
                Interlocked.Increment(ref iThreadCount);
                if (iThreadCount == iMaxCount)
                    ThreadsFinished.Set();
            }
        }
    }

    class VirtualWorker
    {
        public ManualResetEvent ThreadsFinished;
        static Mutex iMutex = new Mutex();
        static List<int> iThreadCount = new List<int> { };
        static List<int> iMaxCount = new List<int> { };
        int iThreadNumber;
        Assembly mCompiledAssembly;
        List<KeyValuePair<String, String>> mLog;
        List<Scenario> mResults;

        public Scenario[] Results { get { return mResults.ToArray(); } }

        public KeyValuePair<String, String>[] Log { get { return mLog.ToArray(); } }

        /// <summary>
        /// Creates new VirtualWorker that will run each virtual user
        /// </summary>
        /// <param name="maxCount">The maximum number of threads expected</param>
        /// <param name="compiledAssembly">Reference to the assembly containing the user code to run</param>
        public VirtualWorker(int maxCount, Assembly compiledAssembly)
        {
            iMutex.WaitOne();
            iThreadNumber = iMaxCount.Count;
            iMaxCount.Add(maxCount);
            iThreadCount.Add(0);
            iMutex.ReleaseMutex();

            mCompiledAssembly = compiledAssembly;
            mLog = new List<KeyValuePair<string, string>> { };
            mResults = new List<Scenario> { };
        }
        /// <summary>
        /// Resets the current thread count
        /// </summary>
        public void Reset()
        {
            ThreadsFinished.Reset();
            iMutex.WaitOne();
            iThreadCount[iThreadNumber] = 0;
            iMutex.ReleaseMutex();
        }

        public void IncreaseMaxCount(int increment)
        {
            iMutex.WaitOne();
            iMaxCount[iThreadNumber] = iMaxCount[iThreadNumber] + increment;
            iMutex.ReleaseMutex();
        }

        public void Worker(object obj)
        {
            Scenario scene = null;
            try
            {
                scene = (Scenario)((object[])obj).GetValue(0);
                String className = (String)((object[])obj).GetValue(1);
                Type _type = mCompiledAssembly.GetType("Script." + className);
                object script = mCompiledAssembly.CreateInstance("Script." + className);
                List<ActionItem> result_actions = new List<ActionItem> { };

                Stopwatch timer = new Stopwatch();
                timer.Start();

                for (int i = 0; i < scene.Actions.Count; i++)
                    result_actions.AddRange(ProcessAction(scene.Actions[i], scene, _type, script));

                timer.Stop();
                scene.RunDuration = timer.Elapsed;
                scene.Actions.Clear();
                scene.Actions.AddRange(result_actions.ToArray());
                mResults.Add(scene);
            }
            catch (Exception e)
            {
                mLog.Add(new KeyValuePair<string, string>("ERROR Processing Scenario: <" + (scene == null ? "null" : scene.Name) + ">",
                    e.Message + "\r\n\r\n" + e.StackTrace));
            }
            finally
            {
                iMutex.WaitOne();
                iThreadCount[iThreadNumber] = iThreadCount[iThreadNumber] + 1;
                iMutex.ReleaseMutex();
                if (iThreadCount[iThreadNumber] == iMaxCount[iThreadNumber])
                    ThreadsFinished.Set();
            }
        }

        ActionItem[] ProcessAction(ActionItem action, Scenario scene, Type _type, object script)
        {
            Stopwatch timer = new Stopwatch();
            timer.Start();
            ActionResults result = new ActionResults();
            List<ActionItem> results_list = new List<ActionItem> { };
            switch (action.ActionType)
            {
                case ActionType.Sleep:
                    Thread.Sleep(((ActionItem_Sleep)action).GetSleepDuration());
                    break;
                case ActionType.Script:
                    try
                    {
                        MethodInfo _methodInfo = _type.GetMethod(((ActionItem_Script)action).MethodName);
                        object returnval = _methodInfo.Invoke(script, new object[] { scene });
                        if (returnval != null)
                            result.Data.Add(returnval);
                    }
                    catch (TargetInvocationException e)
                    {
                        mLog.Add(new KeyValuePair<string, string>("ERROR Running Script: <" + (scene == null ? "null" : scene.Name) +
                            "> Action: <" + (action == null ? "null" : action + ">"), e.Message + "\r\n" + e.InnerException.Message + "\r\n"));
                        result.Errors.Add(e.InnerException.GetType().ToString() + " - " + e.InnerException.Message);
                    }
                    catch (Exception e)
                    {
                        mLog.Add(new KeyValuePair<string, string>("ERROR Running Script: <" + (scene == null ? "null" : scene.Name) +
                            "> Action: <" + (action == null ? "null" : action + ">"), e.Message + "\r\n\r\n" + e.StackTrace));
                        result.Errors.Add(e.GetType().ToString() + " - " + e.Message);
                    }
                    break;
                case ActionType.WebRequest:
                    ActionItem_HttpRequest httpAction = new ActionItem_HttpRequest((ActionItem_WebRequest)action);
                    try
                    {
                        httpAction.Request.CookieContainer = new CookieContainer();
                        foreach (Cookie c in scene.Cookies)
                            httpAction.Request.CookieContainer.Add(c);

                        if (!String.IsNullOrEmpty(httpAction.Content))
                        {
                            byte[] data = Encoding.UTF8.GetBytes(httpAction.Content);
                            Stream stream = httpAction.Request.GetRequestStream();
                            stream.Write(data, 0, data.Length);
                            stream.Close();
                        }
                        HttpWebResponse response = (HttpWebResponse)httpAction.Request.GetResponse();
                        foreach (Cookie cookie in response.Cookies)
                            scene.SetCookie(cookie);

                        result.Data.Add(new HttpResponse(response));
                        mLog.Add(new KeyValuePair<string, string>("Request " + action.Name + " completed successfully with status" + response.StatusCode.ToString(), ""));
                    }
                    catch (Exception e)
                    {
                        result.Errors.Add(e.Message);
                        mLog.Add(new KeyValuePair<string, string>("ERROR: <" + (scene == null ? "null" : scene.Name) +
                            "> Action: <" + (action == null ? "null" : action.Name + ">"), e.Message + "\r\n\r\n" + e.StackTrace));
                    }
                    break;
                case ActionType.If:
                    bool cond = false;
                    ActionItem_If ifAction = (ActionItem_If)action;
                    try
                    {
                        MethodInfo _methodInfo = _type.GetMethod(ifAction.Name);
                        cond = (bool)_methodInfo.Invoke(script, null);
                    }
                    catch (Exception e)
                    {
                        mLog.Add(new KeyValuePair<string, string>("ERROR Processing statement: <" + (scene == null ? "null" : scene.Name) +
                            "> Action: <" + (action == null ? "null" : action + ">"), e.Message + "\r\n\r\n" + e.StackTrace));
                        result.Errors.Add("ERROR Processing statement: " + e.Message);
                    }
                    if (cond)
                        for (int j = 0; j < ifAction.ThenActions.Count; j++)
                            results_list.AddRange(ProcessAction(ifAction.ThenActions[j], scene, _type, script));
                    else
                        for (int j = 0; j < ifAction.ElseActions.Count; j++)
                            results_list.AddRange(ProcessAction(ifAction.ElseActions[j], scene, _type, script));
                    result.Data.Add(cond);
                    break;
                case ActionType.Loop:
                    ActionItem_Loop loopAction = (ActionItem_Loop)action;
                    ActionItemCollection col = new ActionItemCollection();

                    for (int j = 0; j < loopAction.Actions.Count; j++)
                    { col.Add(ObjectCopier.Clone<ActionItem>(loopAction.Actions[j])); }

                    loopAction.Actions.Clear();

                    for (int i = 0; i < loopAction.Count; i++)
                    {
                        for (int j = 0; j < col.Count; j++)
                        {
                            ActionItem item = ObjectCopier.Clone<ActionItem>(col[j]);
                            loopAction.Actions.Add(item);
                            results_list.AddRange(ProcessAction(item, scene, _type, script));
                            item.Name = item.Name + "(" + (i + 1) + ")";
                        }
                    }
                    result.Data.Add(loopAction.Count);
                    break;                    
                case ActionType.Variable:
                    break;
            }
            timer.Stop();
            result.Duration = timer.Elapsed;
            action.Results = result;

            if (results_list.Count == 0)
                return new ActionItem[] { action };
            else
            {
                results_list.Insert(0, action);
                return results_list.ToArray();
            }
        }
    }
}